# encoding: utf-8
"""
@desc: 随机游走算法
@author: 然诺
@contact: rannuo1010@gmail.com
@time: 2019/4/10 23:40
@file: PersonalRank.py
"""

from alg import common
import operator
import matplotlib.pyplot as plt


def get_graph(file_path):
    """
    获取用户——物品二分图
    :param file_path: 文件路径
    :return: 二分图字典
             dict key:user_id,value:{item_id1,item_id2,...}
    """
    # 调用自定义函数读取首行除外的文件数据
    fp = common.read_file(file_path)
    if fp is None:
        return {}
    # 跳过首行
    next(fp)

    G = dict()
    # 按行遍历文件数据
    for line in fp:
        item = line.strip().split(',')
        # 跳过长度不符合要求的行
        if len(item) < 4:
            continue
        user_id, item_id, rating, timestamp = item[0], "item_" + item[1], item[2], item[3]
        # 跳过评分小于阈值（3分）的行
        if float(rating) < 3.0:
            continue
        if user_id not in G:
            G[user_id] = {}
        G[user_id][item_id] = 1
        if item_id not in G:
            G[item_id] = {}
        G[item_id][user_id] = 1

    fp.close()
    return G


def get_item_info(file_path):
    """
    获取物品信息
    :param file_path: 文件路径
    :return: 物品信息字典
             dict key:item_id,value:[title, genre]
    """
    # 调用自定义函数读取首行除外的文件数据
    fp = common.read_file(file_path)
    if fp is None:
        return {}
    # 跳过首行
    next(fp)

    item_info = dict()
    for line in fp:
        # 按逗号切割
        item = line.strip().split(",")
        # 过滤掉长度小于3的行
        if len(item) < 3:
            continue
        elif len(item) == 3:
            item_id, title, genre = item[0], item[1], item[2]
        elif len(item) > 3:
            item_id = item[0]
            genre = item[-1]
            title = ",".join(item[1:-1])
        item_info[item_id] = [title, genre]
    fp.close()
    return item_info


def personal_rank(G, alpha, root, max_setup, top_n):
    """
    基于随机游走的PersonalRank算法
    :param G: 二分图
    :param alpha: 随机游走概率
    :param root: 初始节点
    :param max_setup: 最大走动步数
    :param top_n: 推荐个数
    :return: 推荐结果
             dict key:item_id,value:score
    """
    rec_result = dict()
    rank = dict()
    rank = rank.fromkeys(G.keys(), 0)
    rank[root] = 1

    position = 0
    walk = [position]

    for step in range(max_setup):
        print(step)
        tmp = dict()
        tmp = tmp.fromkeys(G.keys(), 0)
        for node, edges in G.items():
            for next_node, _ in edges.items():
                if next_node not in tmp:
                    tmp[next_node] = 0
                tmp[next_node] += round(alpha * rank[node] / len(edges), 4)
                if next_node == root:
                    tmp[next_node] += round(1 - alpha, 4)
        # 打印迭代收敛的次数
        if tmp == rank:
            print("迭代收敛的次数：" + str(step))
            break
        rank = tmp

        right_num = 0
        for comb in sorted(rank.items(), key=operator.itemgetter(1), reverse=True):
            point, pr_score = comb[0], comb[1]
            if len(point.split('_')) < 2:
                continue
            if point in G[root]:
                continue
            rec_result[point] = pr_score
            right_num += 1
            if right_num > top_n:
                break
        position += step
        walk.append(position)
    return rec_result, walk


def recommend(user_id, user_path, item_path, alpha, max_setup, top_n):
    """
    获取推荐列表
    :param user_id: 用户id
    :param user_path: 用户数据文件路径
    :param item_path: 物品数据文件路径
    :param alpha: 随机游走概率
    :param max_setup: 最大走动步数
    :param top_n: 推荐个数
    :return: 推荐物品及其排行系数
    """
    # 构建二分图
    G = get_graph(user_path)
    # 获取物品信息
    item_graph = get_item_info(item_path)
    # 通过PersonalRank算法计算推荐列表
    rec_result, walk = personal_rank(G, alpha, user_id, max_setup, top_n)
    # 组装推荐列表信息
    rec_list = []
    for item_id in rec_result:
        pure_item_id = item_id.split("_")[1]
        item = item_graph[pure_item_id]
        item.insert(0, rec_result[item_id])
        rec_list.append(item)
    rec_list.sort(key=operator.itemgetter(0), reverse=True)

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    ax.plot(walk)
    plt.show()

    return rec_list[:top_n]


if __name__ == '__main__':
    # user_id, user_path, item_path, alpha, max_setup, rec_num
    rec_list = recommend('1', '../data/ratings.txt', '../data/movies.txt', 0.85, 100, 10)
    print("推荐列表：")
    for item in rec_list:
        print(item)
